package cn.autumnorange.app.user.consumer.authorization.security.security;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.*;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.util.Date;
import java.util.Set;
import java.util.UUID;

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

public class SsoTokenServices
    implements AuthorizationServerTokenServices,
        ResourceServerTokenServices,
        ConsumerTokenServices,
        InitializingBean {
  private int refreshTokenValiditySeconds = 2592000;
  private int accessTokenValiditySeconds = 43200;
  private boolean supportRefreshToken = false;
  private boolean reuseRefreshToken = true;
  private TokenStore tokenStore;
  private ClientDetailsService clientDetailsService;
  private TokenEnhancer accessTokenEnhancer;
  private AuthenticationManager authenticationManager;

  public SsoTokenServices() {}

  @Override
  public void afterPropertiesSet() throws Exception {
    Assert.notNull(this.tokenStore, "tokenStore must be set");
  }

  /**
   * 传入OAuth2的认证对象Authentication authentication生成OAuth2AccessToken
   * 目前只知道此处是登录成功后会调用createAccessToken创建OAuth2AccessToken对其他地方是否有调用本方法不清楚 有也是用来生成OAuth2AccessToken
   * 目前这里只是知道一个账号登录多个不同客服端,就是使用不同客户端clientId会生成不同的OAuth2AccessToken存储在redis中
   * 而且redis不会删除不同客户端的redisToken那么一个账号可以同时拥有不同客户端的token 浏览器都可以拿这些客户端token访问服务端不过服务端应该校验不同客户端需要的权限
   * 对于redis没有删除不同客户端的token这也是合乎现实需求的, 比如淘宝 天猫 开俩个浏览器一个登录淘宝获得clientId淘宝token 一个登录天猫获得clientId天猫token
   * 不可能说我登录天猫客户端后把淘宝客户端的登录挤下去
   *
   * @param authentication
   * @return
   * @throws AuthenticationException
   */
  @Override
  @Transactional
  public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication)
      throws AuthenticationException {
    OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
    OAuth2RefreshToken refreshToken = null;
    if (existingAccessToken != null) {
      //            不存储旧token
      //            if (!existingAccessToken.isExpired()) {
      //                this.tokenStore.storeAccessToken(existingAccessToken, authentication);
      //                return existingAccessToken;
      //            }

      if (existingAccessToken.getRefreshToken() != null) {
        refreshToken = existingAccessToken.getRefreshToken();
        // The token store could remove the refresh token when the
        // access token is removed, but we want to
        // be sure...
        this.tokenStore.removeRefreshToken(refreshToken);
      }
      // every time get new token
      //          对在redis内存数据库查询到的旧existingAccessToken进行删除，
      // 那么浏览器之前存储的这个被删除的旧existingAccessToken再来访问需要权限认证的资源时就无法通过认证了
      // 而当前登录成功后生成的accessToken即为最新的有效token 这样便实现sso单点登录
      // 类比理解：参考qq登录 我先登录我的qq账号 别人知道我的qq账号 在我登录成功之后别人登录我的qq账号qq后台服务端删掉我的登录token
      // 并使用后端推送提示我的账号异地登录，别人登录我的账号成功拿到最新的token
      this.tokenStore.removeAccessToken(existingAccessToken);
    }
    // Only create a new refresh token if there wasn't an existing one
    // associated with an expired access token.
    // Clients might be holding existing refresh tokens, so we re-use it in
    // the case that the old access token
    // expired.
    if (refreshToken == null) {
      refreshToken = this.createRefreshToken(authentication);
    }
    // But the refresh token itself might need to be re-issued if it has
    // expired.
    else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
      ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
      if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
        refreshToken = this.createRefreshToken(authentication);
      }
    }
    // redis存储本次生成的OAuth2AccessToken
    OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
    this.tokenStore.storeAccessToken(accessToken, authentication);
    // In case it was modified
    refreshToken = accessToken.getRefreshToken();
    if (refreshToken != null) {
      this.tokenStore.storeRefreshToken(refreshToken, authentication);
    }

    return accessToken;
  }

  @Transactional(noRollbackFor = {InvalidTokenException.class, InvalidGrantException.class})
  public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
      throws AuthenticationException {
    if (!this.supportRefreshToken) {
      throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
    } else {
      OAuth2RefreshToken refreshToken = this.tokenStore.readRefreshToken(refreshTokenValue);
      if (refreshToken == null) {
        throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
      } else {
        OAuth2Authentication authentication =
            this.tokenStore.readAuthenticationForRefreshToken(refreshToken);
        if (this.authenticationManager != null && !authentication.isClientOnly()) {
          Authentication user =
              new PreAuthenticatedAuthenticationToken(
                  authentication.getUserAuthentication(), "", authentication.getAuthorities());
          user = this.authenticationManager.authenticate(user);
          Object details = authentication.getDetails();
          authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
          authentication.setDetails(details);
        }

        String clientId = authentication.getOAuth2Request().getClientId();
        if (clientId != null && clientId.equals(tokenRequest.getClientId())) {
          this.tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
          if (this.isExpired(refreshToken)) {
            this.tokenStore.removeRefreshToken(refreshToken);
            throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
          } else {
            authentication = this.createRefreshedAuthentication(authentication, tokenRequest);
            if (!this.reuseRefreshToken) {
              this.tokenStore.removeRefreshToken(refreshToken);
              refreshToken = this.createRefreshToken(authentication);
            }

            OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
            this.tokenStore.storeAccessToken(accessToken, authentication);
            if (!this.reuseRefreshToken) {
              this.tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
            }

            return accessToken;
          }
        } else {
          throw new InvalidGrantException(
              "Wrong client for this refresh token: " + refreshTokenValue);
        }
      }
    }
  }

  public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
    return this.tokenStore.getAccessToken(authentication);
  }

  private OAuth2Authentication createRefreshedAuthentication(
      OAuth2Authentication authentication, TokenRequest request) {
    Set<String> scope = request.getScope();
    OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(request);
    if (scope != null && !scope.isEmpty()) {
      Set<String> originalScope = clientAuth.getScope();
      if (originalScope == null || !originalScope.containsAll(scope)) {
        throw new InvalidScopeException(
            "Unable to narrow the scope of the client authentication to " + scope + ".",
            originalScope);
      }

      clientAuth = clientAuth.narrowScope(scope);
    }

    OAuth2Authentication narrowed =
        new OAuth2Authentication(clientAuth, authentication.getUserAuthentication());
    return narrowed;
  }

  protected boolean isExpired(OAuth2RefreshToken refreshToken) {
    if (!(refreshToken instanceof ExpiringOAuth2RefreshToken)) {
      return false;
    } else {
      ExpiringOAuth2RefreshToken expiringToken = (ExpiringOAuth2RefreshToken) refreshToken;
      return expiringToken.getExpiration() == null
          || System.currentTimeMillis() > expiringToken.getExpiration().getTime();
    }
  }

  public OAuth2AccessToken readAccessToken(String accessToken) {
    return this.tokenStore.readAccessToken(accessToken);
  }

  public OAuth2Authentication loadAuthentication(String accessTokenValue)
      throws AuthenticationException, InvalidTokenException {
    OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue);
    if (accessToken == null) {
      throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
    } else if (accessToken.isExpired()) {
      this.tokenStore.removeAccessToken(accessToken);
      throw new InvalidTokenException("Access token expired: " + accessTokenValue);
    } else {
      OAuth2Authentication result = this.tokenStore.readAuthentication(accessToken);
      if (result == null) {
        throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
      } else {
        if (this.clientDetailsService != null) {
          String clientId = result.getOAuth2Request().getClientId();

          try {
            this.clientDetailsService.loadClientByClientId(clientId);
          } catch (ClientRegistrationException var6) {
            throw new InvalidTokenException("Client not valid: " + clientId, var6);
          }
        }

        return result;
      }
    }
  }

  public String getClientId(String tokenValue) {
    OAuth2Authentication authentication = this.tokenStore.readAuthentication(tokenValue);
    if (authentication == null) {
      throw new InvalidTokenException("Invalid access token: " + tokenValue);
    } else {
      OAuth2Request clientAuth = authentication.getOAuth2Request();
      if (clientAuth == null) {
        throw new InvalidTokenException("Invalid access token (no client id): " + tokenValue);
      } else {
        return clientAuth.getClientId();
      }
    }
  }

  public boolean revokeToken(String tokenValue) {
    OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(tokenValue);
    if (accessToken == null) {
      return false;
    } else {
      if (accessToken.getRefreshToken() != null) {
        this.tokenStore.removeRefreshToken(accessToken.getRefreshToken());
      }

      this.tokenStore.removeAccessToken(accessToken);
      return true;
    }
  }

  private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
    if (!this.isSupportRefreshToken(authentication.getOAuth2Request())) {
      return null;
    } else {
      int validitySeconds = this.getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
      String value = UUID.randomUUID().toString();
      return (OAuth2RefreshToken)
          (validitySeconds > 0
              ? new DefaultExpiringOAuth2RefreshToken(
                  value, new Date(System.currentTimeMillis() + (long) validitySeconds * 1000L))
              : new DefaultOAuth2RefreshToken(value));
    }
  }

  private OAuth2AccessToken createAccessToken(
      OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
    DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
    int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
    if (validitySeconds > 0) {
      token.setExpiration(new Date(System.currentTimeMillis() + (long) validitySeconds * 1000L));
    }

    token.setRefreshToken(refreshToken);
    token.setScope(authentication.getOAuth2Request().getScope());
    return (OAuth2AccessToken)
        (this.accessTokenEnhancer != null
            ? this.accessTokenEnhancer.enhance(token, authentication)
            : token);
  }

  protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {
    if (this.clientDetailsService != null) {
      ClientDetails client =
          this.clientDetailsService.loadClientByClientId(clientAuth.getClientId());
      Integer validity = client.getAccessTokenValiditySeconds();
      if (validity != null) {
        return validity;
      }
    }

    return this.accessTokenValiditySeconds;
  }

  protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) {
    if (this.clientDetailsService != null) {
      ClientDetails client =
          this.clientDetailsService.loadClientByClientId(clientAuth.getClientId());
      Integer validity = client.getRefreshTokenValiditySeconds();
      if (validity != null) {
        return validity;
      }
    }

    return this.refreshTokenValiditySeconds;
  }

  protected boolean isSupportRefreshToken(OAuth2Request clientAuth) {
    if (this.clientDetailsService != null) {
      ClientDetails client =
          this.clientDetailsService.loadClientByClientId(clientAuth.getClientId());
      return client.getAuthorizedGrantTypes().contains("refresh_token");
    } else {
      return this.supportRefreshToken;
    }
  }

  public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) {
    this.accessTokenEnhancer = accessTokenEnhancer;
  }

  public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) {
    this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
  }

  public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) {
    this.accessTokenValiditySeconds = accessTokenValiditySeconds;
  }

  public void setSupportRefreshToken(boolean supportRefreshToken) {
    this.supportRefreshToken = supportRefreshToken;
  }

  public void setReuseRefreshToken(boolean reuseRefreshToken) {
    this.reuseRefreshToken = reuseRefreshToken;
  }

  public void setTokenStore(TokenStore tokenStore) {
    this.tokenStore = tokenStore;
  }

  public void setAuthenticationManager(AuthenticationManager authenticationManager) {
    this.authenticationManager = authenticationManager;
  }

  public void setClientDetailsService(ClientDetailsService clientDetailsService) {
    this.clientDetailsService = clientDetailsService;
  }
}
